﻿// -----------------------------------------------------------------------
// <copyright file="AudioPlayer.cs" company="Abyssalsoft">
// TODO: Update copyright text.
// </copyright>
// -----------------------------------------------------------------------

namespace Core.Player.Audio
{
    using System;
    using System.Collections.Generic;
    using System.Runtime.InteropServices;
    using System.Text;
    using Filters;
    using Un4seen.Bass;
    using Un4seen.Bass.AddOn.Tags;
    using Un4seen.Bass.AddOn.Wma;
    using Un4seen.Bass.Misc;

    /// <summary>
    /// Concrete implementation of Player
    /// </summary>
    internal class AudioPlayer : Player
    {
        /// <summary>Buffer - 1Mb should be enough</summary>
        private const int StreamBuffer = 1048510;

        /// <summary>User agent pointer</summary>
        private IntPtr userAgentPtr;

        /// <summary>WMA plugin</summary>
        private int wmaPlugIn = 0;

        /// <summary>Declaration of the delegate DOWNLOADPROC</summary>
        private DOWNLOADPROC streamCreateURL;

        /// <summary>Stores the number of bytes written</summary>
        private int bytesWritten = 0;

        /// <summary>Declaration of the tags info class</summary>
        private TAG_INFO tagInfo;

        /// <summary>Delegate synchronize process</summary>
        private SYNCPROC sync;

        /// <summary>Delegate record process</summary>
        private RECORDPROC recProc;

        /// <summary>Initialize the recording buffer</summary>
        private byte[] recbuffer = new byte[StreamBuffer];

        /// <summary>Our pointer</summary>
        private IntPtr intPtrHandle = IntPtr.Zero;

        /// <summary>The file type of the current playback</summary>
        private Helper.FileTypeEnum fileType;

        /// <summary> Initializes a new instance of the AudioPlayer class. </summary>
        internal AudioPlayer()
        {
            // Activate our licence
            Un4seen.Bass.BassNet.Registration("marius@abyssalsoft.com", "2X223282231122");

            // Init BASS
            if (Bass.BASS_Init(-1, 44100, BASSInit.BASS_DEVICE_LATENCY, this.intPtrHandle))
            {
                // Init succes
            }
            else
            {
                // Initialisation failed. Fuck that, something's wrong with bass library.
            }
        }

        /// <summary>Update tags</summary>
        private delegate void UpdateTagDelegate();

        /// <summary>Update status</summary>
        /// <param name="txt">Status received</param>
        private delegate void UpdateStatusDelegate(string txt);

        /// <summary>Update message</summary>
        /// <param name="txt">Message received</param>
        private delegate void UpdateMessageDelegate(string txt);

        /// <summary>Gets or sets the user agent pointer</summary>
        private IntPtr UserAgentPtr
        {
            get
            {
                return this.userAgentPtr;
            }

            set
            {
                this.userAgentPtr = value;
            }
        }

        /// <summary>
        /// Open file
        /// </summary>
        /// <param name="file">The file path to open</param>
        /// <param name="fileType">The file type of the file</param>
        internal override void Open(string file, Helper.FileTypeEnum fileType)
        {
            // Call before open event
            if (Events.Instance.BeforeOpen != null)
            {
                Events.Instance.BeforeOpen(null, null);
            }

            // Reset
            MediaInfo.Artist = string.Empty;
            MediaInfo.Title = string.Empty;

            this.fileType = fileType;

            MediaInfo.FilePath = file;

            if (fileType == Helper.FileTypeEnum.LocalAudio)
            {
                AudioCommon.Instance.Stream = Bass.BASS_StreamCreateFile(file, 0, 0, BASSFlag.BASS_DEFAULT);
                if (AudioCommon.Instance.Stream != 0)
                {
                    // This is something like event, we need to know when the song is done playing
                    this.sync = new SYNCPROC(this.OnSongEnded);
                    Bass.BASS_ChannelSetSync(AudioCommon.Instance.Stream, BASSSync.BASS_SYNC_END, 0, this.sync, IntPtr.Zero);
                }
                else
                {
                    // Error loading file!
                }
            }
            else if (fileType == Helper.FileTypeEnum.InternetAudioStream)
            {
                // Internet streaming
                this.StartStreaming(file);
            }

            // Call open event
            if (Events.Instance.Open != null)
            {
                Events.Instance.Open(null, null);
            }

            // Update volume
            this.SetVolume(Status.Volume);
        }

        /// <summary>Play media file</summary>
        internal override void Play()
        {
            // Get the tags
            this.tagInfo = new TAG_INFO(MediaInfo.FileName);
            if (BassTags.BASS_TAG_GetFromFile(AudioCommon.Instance.Stream, this.tagInfo))
            {
                this.UpdateTagDisplay();
            }

            if (fileType == Helper.FileTypeEnum.LocalAudio)
            {
                // Set file name for local audio
                if (MediaInfo.Artist != string.Empty)
                {
                    MediaInfo.FileName = MediaInfo.Artist + " - " + MediaInfo.Title;
                }
                else
                {
                    MediaInfo.FileName = System.IO.Path.GetFileNameWithoutExtension(MediaInfo.FilePath);
                }
            }
            else
            {
                // Set file name for online streaming
                if (MediaInfo.Artist != string.Empty)
                {
                    MediaInfo.FileName = MediaInfo.Artist + " - " + MediaInfo.Title;
                }
                else
                {
                    // The radio does not send ID3 tags info, so we show the radio name
                    MediaInfo.Artist = MediaInfo.RadioName;
                    MediaInfo.FileName = MediaInfo.RadioName;
                }
            }

            // Play
            Bass.BASS_ChannelPlay(AudioCommon.Instance.Stream, false);
            if (Events.Instance.Play != null)
            {
                Events.Instance.Play(null, null);
            }
        }

        /// <summary>Pause playback</summary>
        internal override void Pause()
        {
            Bass.BASS_ChannelPause(AudioCommon.Instance.Stream);
            if (Events.Instance.Pause != null)
            {
                Events.Instance.Pause(null, null);
            }
        }

        /// <summary>Stop playback</summary>
        internal override void Stop()
        {
            if (AudioCommon.Instance.Stream != 0)
            {
                Bass.BASS_ChannelStop(AudioCommon.Instance.Stream);
                if (Events.Instance.Stop != null)
                {
                    Events.Instance.Stop(null, null);
                }
            }
        }

        /// <summary>Resume playback</summary>
        internal override void Resume()
        {
            Bass.BASS_ChannelPlay(AudioCommon.Instance.Stream, false);
            if (Events.Instance.Resume != null)
            {
                Events.Instance.Resume(null, null);
            }
        }

        /// <summary>Set audio volume</summary>
        /// <param name="value">New volume</param>
        internal override void SetVolume(int value)
        {
            if (AudioCommon.Instance.Stream == -1)
            {
                return;
            }

            float val = value / 100.0F;
            Bass.BASS_ChannelSetAttribute(AudioCommon.Instance.Stream, Un4seen.Bass.BASSAttribute.BASS_ATTRIB_VOL, val);
        }

        /// <summary>Seek current media to specified position</summary>
        /// <param name="value">New position value</param>
        internal override void Seek(int value)
        {
            Bass.BASS_ChannelSetPosition(AudioCommon.Instance.Stream, Bass.BASS_ChannelSeconds2Bytes(AudioCommon.Instance.Stream, value), BASSMode.BASS_POS_BYTES);
        }

        /// <summary>Switch video full screen mode (not used here)</summary>
        internal override void SetVideoFullscreen()
        {
            // Do nothing
        }

        /// <summary>End playback and release the resources</summary>
        internal override void Dispose()
        {
            this.Stop();
            Bass.BASS_Free();
        }

        /// <summary>
        /// Calculates the total time of the media in seconds
        /// </summary>
        /// <returns>Returns the total time transformed in seconds</returns>
        internal override double GetTotalTimeInSeconds()
        {
            return Bass.BASS_ChannelBytes2Seconds(AudioCommon.Instance.Stream, Bass.BASS_ChannelGetLength(AudioCommon.Instance.Stream));
        }

        /// <summary>
        /// Calculates the total time of the media in bytes
        /// </summary>
        /// <returns>Returns the total time transformed in bytes</returns>
        internal override long GetTotalTimeInBytes()
        {
            return Bass.BASS_ChannelGetLength(AudioCommon.Instance.Stream);
        }

        /// <summary>
        /// Calculates the current position of the media in seconds
        /// </summary>
        /// <returns>Returns the current position transformed in seconds</returns>
        internal override double GetCurrentPositionInSeconds()
        {
            return Bass.BASS_ChannelBytes2Seconds(AudioCommon.Instance.Stream, Status.CurrentPositionInBytes);
        }

        /// <summary>
        /// Calculates the current position of the media in bytes
        /// </summary>
        /// <returns>Returns the current position transformed in bytes</returns>
        internal override long GetCurrentPositionInBytes()
        {
            return Bass.BASS_ChannelGetPosition(AudioCommon.Instance.Stream);
        }

        /// <summary>
        /// Calculates the total time of the media formatted
        /// </summary>
        /// <returns>Returns the total time of the current playback formatted</returns>
        internal override string GetTotalTimeFormatted()
        {
            return string.Format("{0:#0.00}", Utils.FixTimespan(this.GetTotalTimeInSeconds(), "HMMSS"));
        }

        /// <summary>
        /// Calculates the current position of the media formatted
        /// </summary>
        /// <returns>Returns the current position of the current playback formatted</returns>
        internal override string GetCurrentPositionFormatted()
        {
            return string.Format("{0:#0.00}", Utils.FixTimespan(this.GetCurrentPositionInSeconds(), "HMMSS"));
        }

        /// <summary>This is called when the file reach the end</summary>
        /// <param name="handle">Handler for this stream</param>
        /// <param name="channel">Channel integer</param>
        /// <param name="data">Data integer</param>
        /// <param name="user">The user name</param>
        private void OnSongEnded(int handle, int channel, int data, IntPtr user)
        {
            if (Events.Instance.PlaybackEnded != null)
            {
                Status.PlaybackStatus = Status.StatusEnum.Stopped;
                Events.Instance.PlaybackEnded(this, null);
            }
        }

        #region Radio methods

        /// <summary>
        /// Begin playback
        /// </summary>
        /// <param name="file">The file to stream</param>
        private void StartStreaming(string file)
        {
            Bass.BASS_SetConfigPtr(BASSConfig.BASS_CONFIG_NET_AGENT, this.userAgentPtr);

            Bass.BASS_SetConfig(BASSConfig.BASS_CONFIG_NET_PREBUF, 0); // so that we can display the buffering%
            Bass.BASS_SetConfig(BASSConfig.BASS_CONFIG_NET_PLAYLIST, 1);

            this.wmaPlugIn = Bass.BASS_PluginLoad("bass.aac.dll");

            if (Bass.BASS_SetConfig(BASSConfig.BASS_CONFIG_WMA_PREBUF, 0) == false)
            {
                // Is an error, try WMA plugin
                this.wmaPlugIn = Bass.BASS_PluginLoad("bass.wma.dll");
                Bass.BASS_SetConfig(BASSConfig.BASS_CONFIG_WMA_PREBUF, 0);
            }

            // We already create the user callback methods...
            this.streamCreateURL = new DOWNLOADPROC(this.DownloadProc);

            // Play stream
            Bass.BASS_StreamFree(AudioCommon.Instance.Stream);

            bool isWMA = false;
            if (file == string.Empty)
            {
                return;
            }

            MediaInfo.RadioChannelInfo += "URL: " + file + Environment.NewLine;

            // Create the stream
            AudioCommon.Instance.Stream = Bass.BASS_StreamCreateURL(file, 0, BASSFlag.BASS_STREAM_STATUS, this.streamCreateURL, IntPtr.Zero);
            if (AudioCommon.Instance.Stream == 0)
            {
                // Try WMA streams...
                AudioCommon.Instance.Stream = BassWma.BASS_WMA_StreamCreateFile(file, 0, 0, BASSFlag.BASS_DEFAULT);
                if (AudioCommon.Instance.Stream != 0)
                {
                    isWMA = true;
                }
                else
                {
                    // Error
                    return;
                }
            }

            this.tagInfo = new TAG_INFO(file);
            BASS_CHANNELINFO info = Bass.BASS_ChannelGetInfo(AudioCommon.Instance.Stream);
            if (info.ctype == BASSChannelType.BASS_CTYPE_STREAM_WMA)
            {
                isWMA = true;
            }

            // Ok, do some pre-buffering...
            if (!isWMA)
            {
                // Display buffering for MP3, OGG...
                while (true)
                {
                    long len = Bass.BASS_StreamGetFilePosition(AudioCommon.Instance.Stream, BASSStreamFilePosition.BASS_FILEPOS_END);
                    if (len == -1)
                    {
                        break; // Typical for WMA streams :|
                    }

                    // Percentage of buffer filled
                    float progress = (
                        Bass.BASS_StreamGetFilePosition(AudioCommon.Instance.Stream, BASSStreamFilePosition.BASS_FILEPOS_DOWNLOAD) -
                        Bass.BASS_StreamGetFilePosition(AudioCommon.Instance.Stream, BASSStreamFilePosition.BASS_FILEPOS_CURRENT)) * 100f / len;

                    if (progress > 75f)
                    {
                        // Over 75% full, enough
                        break;
                    }
                }
            }
            else
            {
                // Display buffering for WMA...
                while (true)
                {
                    long len = Bass.BASS_StreamGetFilePosition(AudioCommon.Instance.Stream, BASSStreamFilePosition.BASS_FILEPOS_WMA_BUFFER);
                    if (len == -1L)
                    {
                        break;
                    }

                    // Percentage of buffer filled
                    if (len > 75L)
                    {
                        // Over 75% full, enough
                        break;
                    }
                }
            }

            this.ChannelGetTagsICY();

            if (BassTags.BASS_TAG_GetFromURL(AudioCommon.Instance.Stream, this.tagInfo))
            {
                // Stores what we get
                this.UpdateTagDisplay();
            }

            // Set a sync to get the title updates out of the meta data...
            this.sync = new SYNCPROC(this.MetaSync);
            Bass.BASS_ChannelSetSync(AudioCommon.Instance.Stream, BASSSync.BASS_SYNC_META, 0, this.sync, IntPtr.Zero);
            Bass.BASS_ChannelSetSync(AudioCommon.Instance.Stream, BASSSync.BASS_SYNC_WMA_CHANGE, 0, this.sync, IntPtr.Zero);

            // Start "recording"...
            int rechandle = 0;
            if (Bass.BASS_RecordInit(-1))
            {
                this.bytesWritten = 0;
                this.recProc = new RECORDPROC(this.Recording);
                rechandle = Bass.BASS_RecordStart(44100, 2, BASSFlag.BASS_RECORD_PAUSE, this.recProc, IntPtr.Zero);
            }

            // Record the stream
            Bass.BASS_ChannelPlay(rechandle, false);
        }

        /// <summary>
        /// Get some meta information of the current channel
        /// </summary>
        private void ChannelGetTagsICY()
        {
            // Get the meta tags (manually - will not work for WMA streams here)
            string[] icy = Bass.BASS_ChannelGetTagsICY(AudioCommon.Instance.Stream);
            if (icy == null)
            {
                // Try http...
                icy = Bass.BASS_ChannelGetTagsHTTP(AudioCommon.Instance.Stream);
            }

            if (icy != null)
            {
                foreach (string tag in icy)
                {
                    MediaInfo.RadioChannelInfo += "ICY: " + tag + Environment.NewLine;
                }
            }

            // Get the initial meta data (streamed title...)
            icy = Bass.BASS_ChannelGetTagsMETA(AudioCommon.Instance.Stream);
            if (icy != null)
            {
                foreach (string tag in icy)
                {
                    MediaInfo.RadioChannelInfo += "Meta: " + tag + Environment.NewLine;
                }
            }
            else
            {
                // Obtain ogg stream meta
                icy = Bass.BASS_ChannelGetTagsOGG(AudioCommon.Instance.Stream);
                if (icy != null)
                {
                    foreach (string tag in icy)
                    {
                        MediaInfo.RadioChannelInfo += "Meta: " + tag + Environment.NewLine;
                    }
                }
            }
        }

        /// <summary>Download stream</summary>
        /// <param name="buffer">Buffer pointer</param>
        /// <param name="length">Length of this stream</param>
        /// <param name="user">The user name</param>
        private void DownloadProc(IntPtr buffer, int length, IntPtr user)
        {
            if (buffer != IntPtr.Zero && length == 0)
            {
                // The buffer contains HTTP or ICY tags.
                string txt = Marshal.PtrToStringAnsi(buffer);

                ////this.Invoke = (new UpdateMessageDelegate(this.UpdateMessageDisplay), new object[] { txt });

                this.recProc = new RECORDPROC(this.Recording);
            }
        }

        /// <summary>Start / stop recording</summary>
        /// <param name="handle">Handler for this recording</param>
        /// <param name="buffer">Buffer pointer</param>
        /// <param name="length">Length of this stream</param>
        /// <param name="user">The user that records</param>
        /// <returns>Returns a boolean, true if recording, false if has done recording</returns>
        private bool Recording(int handle, IntPtr buffer, int length, IntPtr user)
        {
            // Just a dummy here, nothing is really written to disk.
            if (length > 0 && buffer != IntPtr.Zero)
            {
                // Copy from managed to unmanaged memory
                // It is clever to NOT alloc the byte[] everytime here, since ALL callbacks should be really fast!
                // and if you would do a 'new byte[]' every time here...the GarbageCollector would never really clean up that memory here
                // even other sideeffects might occure, due to the fact, that BASS micht call this callback too fast and too often...
                Marshal.Copy(buffer, this.recbuffer, 0, length);

                // Write to file
                // NOT implemented...
                this.bytesWritten += length;

                ////Console.WriteLine("Bytes written = {0}", this.byteswritten);
                if (this.bytesWritten < 800000)
                {
                    return true; // continue recording
                }
                else
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>Synchronize meta tags on SHOUT cast streams</summary>
        /// <param name="handle">Handler for this stream</param>
        /// <param name="channel">Channel integer</param>
        /// <param name="data">Data integer</param>
        /// <param name="user">The user name</param>
        private void MetaSync(int handle, int channel, int data, IntPtr user)
        {
            // BASS_SYNC_META is triggered on meta changes of SHOUTcast streams
            if (this.tagInfo.UpdateFromMETA(Bass.BASS_ChannelGetTags(channel, BASSTag.BASS_TAG_META), false, true))
            {
                // The song had been changed, update id3tags
                this.UpdateTagDisplay();
            }
        }

        /// <summary>Stores the ID tags</summary>
        private void UpdateTagDisplay()
        {
            // Update ID3 Tags
            MediaInfo.Album = this.tagInfo.album;
            MediaInfo.Artist = this.tagInfo.artist;
            MediaInfo.Title = this.tagInfo.title;
            MediaInfo.Comment = this.tagInfo.comment;
            MediaInfo.Genre = this.tagInfo.genre;
            MediaInfo.Year = this.tagInfo.year;
            MediaInfo.Track = this.tagInfo.track;
            MediaInfo.BitRate = this.tagInfo.bitrate.ToString();

            if (fileType == Helper.FileTypeEnum.InternetAudioStream)
            {
                if (MediaInfo.Artist != string.Empty && MediaInfo.Title != string.Empty)
                {
                    MediaInfo.FileName = MediaInfo.Artist + " - " + MediaInfo.Title;
                }
                else
                {
                    MediaInfo.FileName = MediaInfo.RadioName;
                }
            }

            // Update other info
            BASS_CHANNELINFO info = new BASS_CHANNELINFO();
            Bass.BASS_ChannelGetInfo(AudioCommon.Instance.Stream, info);

            // Transform hz to Khz
            int khz = info.freq / 1000;

            string Hz = khz.ToString() + " KHz";
            string Bytes = string.Empty;
            string Channel = string.Empty;

            if (info.chans == 1)
            {
                Channel = "Mono";
            }

            if (info.chans == 2)
            {
                Channel = "Stereo";
            }

            if (info.Is32bit == true)
            {
                Bytes = "32 bits";
            }

            if (info.Is8bit == true)
            {
                Bytes = "8 bits";
            }

            if (Bytes == string.Empty)
            {
                Bytes = "16 bits";
            }
            MediaInfo.Frequency = Hz;
            MediaInfo.Bytes = Bytes;
            MediaInfo.Channel = Channel;
            
            if (MediaInfo.CurrentFile == Helper.FileTypeEnum.InternetAudioStream)
            {
                Events.Instance.RadioSongChanged(null, null);
            }
        }

        #endregion
    }
}
